/*
    RoleCall provides a web service that web applications can use to find 
    the roles associated with a user that has been authenticated via OpenID. 
    Also provides a web-based user interface to manage the user roles 
    associated with partner web applications.
    
    Development of this software was supported in part by the David and Lucile 
    Packard Foundation and by the the Office Of Naval Research (ONR) 
     
    Copyright (c) 2012, 
    Monterey Bay Aquarium Research Institute - MBARI ( www.mbari.org )
    Michael Godin ( mikegodin AT users DOT sourceforge DOT net )

    RoleCall is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    RoleCall is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Foobar (see the file COPYING.GPL).  If not, see 
    <http://www.gnu.org/licenses/>.  
 */

package org.mbari.servlet;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.mbari.rolecall.model.User;
import org.mbari.rolecall.server.PersistentPropertyStore;
import org.mbari.rolecall.server.RoleCallConsumer;

import com.dyuproject.openid.OpenIdServletFilter;
import com.dyuproject.openid.OpenIdUser;
import com.dyuproject.openid.RelyingParty;
import com.dyuproject.openid.ext.AxSchemaExtension;
import com.dyuproject.openid.ext.SRegExtension;
import com.dyuproject.openid.ext.SRegExtension10;

@SuppressWarnings("serial")
abstract public class HomeServlet extends HttpServlet implements Servlet {

    static protected String compileTimestamp;

    static RelyingParty relayingParty = RelyingParty.getInstance();

    // Should return name and email from a variety of openID providers.
    static {

        RelyingParty.getInstance().addListener(
                new SRegExtension10().addExchange("email").addExchange(
                        "fullname"));
        RelyingParty.getInstance().addListener(
                new SRegExtension().addExchange("email")
                        .addExchange("fullname"));
        RelyingParty.getInstance().addListener(
                new AxSchemaExtension().addExchange("email")
                        .addExchange("fullname").addExchange("firstname")
                        .addExchange("lastname"));

        Date compileTime = null;
        try {
            compileTime = calcCompileTimeStamp(HomeServlet.class);
        } catch (IOException ioe) {
        }
        compileTimestamp = compileTime == null ? Double.toString(Math.random())
                : Long.toString(compileTime.getTime() / 1000);
    }

    static protected void addGwtCodeSvr(final HttpServletRequest req,
            StringBuffer requestURL) throws UnsupportedEncodingException {
        String gwtCodeSvr = req.getParameter("gwt.codesvr");
        if (null != gwtCodeSvr) {
            requestURL.append("?gwt.codesvr=").append(gwtCodeSvr);
        }
    }

    protected static Date calcCompileTimeStamp(final Class<?> cls)
            throws IOException {
        ClassLoader loader = cls.getClassLoader();
        String filename = cls.getName().replace('.', '/') + ".class";
        // get the corresponding class file as a Resource.
        URL resource = (loader != null) ? loader.getResource(filename)
                : ClassLoader.getSystemResource(filename);
        URLConnection connection = resource.openConnection();
        // Note, we are using Connection.getLastModified not File.lastModifed.
        // This will then work both or members of jars or standalone class
        // files.
        long time = connection.getLastModified();
        return (time != 0L) ? new Date(time) : null;
    }

    static protected boolean cookieLogin(final HttpServletRequest req,
            final HttpServletResponse resp) throws IOException {
        if ("true".equals(req.getSession().getAttribute("triedCookieLogin"))) {
            return false;
        }
        Cookie[] cookies = req.getCookies();
        if (null != cookies) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("openid_identifier")
                        && !cookie.getValue().isEmpty()) {
                    String openID = URLDecoder.decode(cookie.getValue(),
                            "UTF-8");
                    StringBuffer requestURL = req.getRequestURL();
                    addGwtCodeSvr(req, requestURL);
                    resp.setContentType("text/html");
                    ServletOutputStream out = resp.getOutputStream();
                    out.println("<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'"
                            + "'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'>");
                    out.println("<html xmlns='http://www.w3.org/1999/xhtml'>");
                    out.println("<head>");
                    out.println("  <title>OpenID HTML FORM Redirection</title>");
                    out.println("</head>");
                    out.println("<body onload='document.forms[\"openid-form-redirection\"].submit();'>");
                    out.println("  <form name='openid-form-redirection' action='"
                            + requestURL.toString()
                            + "' method='post' accept-charset='utf-8'>");
                    out.println("    <input type='hidden' name='openid_identifier' value='"
                            + openID + "'/>");
                    out.println("    <button type='submit'>Continue...</button>");
                    out.println("  </form>");
                    out.println("</body>");
                    out.println("</html>");
                    req.getSession().setAttribute("triedCookieLogin", "true");
                    return true;
                }
            }
        }
        return false;
    }

    protected String homeUri;

    protected String loginUri;

    protected boolean checkConfiguration(final HttpServletRequest req) {
        return RoleCallConsumer.checkConfiguration(req, getPropertyStore());
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest
     * , javax.servlet.http.HttpServletResponse)
     */
    @Override
    protected void doGet(final HttpServletRequest req,
            final HttpServletResponse resp) throws ServletException,
            IOException {
        doPost(req, resp);
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest
     * , javax.servlet.http.HttpServletResponse)
     */
    @Override
    protected void doPost(final HttpServletRequest req,
            final HttpServletResponse resp) throws ServletException,
            IOException {

        if (!checkConfiguration(req)) {
            generateConfigPage(req, resp);
            return;
        }

        if ("true".equals(req.getParameter("logout"))) {
            relayingParty.invalidate(req, resp);
            setSessionUser(req, null);
            reload(req, resp, null);
            return;
        }

        String openIdErrorMsg = OpenIdServletFilter.DEFAULT_ERROR_MSG;
        try {
            OpenIdUser openIdUser = relayingParty.discover(req);
            if (openIdUser == null) {
                if (RelyingParty.isAuthResponse(req)) {
                    // authentication timeout
                    resp.sendRedirect(req.getRequestURI());
                } else {
                    // set error msg if the openid_identifier is not resolved.
                    if (req.getParameter(relayingParty.getIdentifierParameter()) != null) {
                        req.setAttribute(OpenIdServletFilter.ERROR_MSG_ATTR,
                                openIdErrorMsg);
                        forwardLogin(req, resp);
                    } else if (!cookieLogin(req, resp)) {
                        if (RelyingParty.isAuthCancel(req)) {
                            setSessionUser(req, null);
                            relayingParty.invalidate(req, resp);
                            reload(req, resp, null);
                        } else {
                            // new openIdUser
                            forwardLogin(req, resp);
                        }
                    }
                }
                return;
            }

            if (openIdUser.isAuthenticated()) {
                // openIdUser already authenticated
                User user = openIdUserToUser(openIdUser);
                setSessionUser(req, user);
                RoleCallConsumer.assignRoles(req, user, getPropertyStore());
                forwardHome(req, resp, openIdUser);
                return;
            }

            if (openIdUser.isAssociated()) {
                if (RelyingParty.isAuthResponse(req)) {
                    // verify authentication
                    if (relayingParty.verifyAuth(openIdUser, req, resp)) {
                        // authenticated
                        User user = openIdUserToUser(openIdUser);
                        setSessionUser(req, user);
                        reload(req, resp, openIdUser);
                    } else {
                        forwardLogin(req, resp);
                    }
                    return;
                }
            }

            // associate and authenticate openIdUser
            StringBuffer requestURL = req.getRequestURL();
            String trustRoot = requestURL.substring(0,
                    requestURL.indexOf("/", 9));
            String realm = requestURL.substring(0, requestURL.lastIndexOf("/"));
            addGwtCodeSvr(req, requestURL);
            String returnTo = requestURL.toString();
            if ("true".equals(req.getSession().getAttribute(
                    "triedAuthenticating"))) {
                req.getSession().removeAttribute("triedAuthenticating");
                for (Cookie cookie : req.getCookies()) {
                    if (cookie.getName().equals("openid_identifier")
                            && !cookie.getValue().isEmpty()) {
                        cookie.setMaxAge(0);
                        cookie.setValue("");
                        break;
                    }
                }
                forwardLogin(req, resp);
                return;
            }

            req.getSession().setAttribute("triedAuthenticating", "true");
            if (relayingParty.associateAndAuthenticate(openIdUser, req, resp,
                    trustRoot, realm, returnTo)) {
                // successful association
                User user = openIdUserToUser(openIdUser);
                setSessionUser(req, user);
                return;
            }
            req.getSession().removeAttribute("triedAuthenticating");
        } catch (UnknownHostException uhe) {
            System.err.println("not found");
            openIdErrorMsg = OpenIdServletFilter.ID_NOT_FOUND_MSG;
        } catch (FileNotFoundException fnfe) {
            System.err.println("could not be resolved");
            openIdErrorMsg = OpenIdServletFilter.DEFAULT_ERROR_MSG;
        } catch (Exception e) {
            e.printStackTrace();
            openIdErrorMsg = OpenIdServletFilter.DEFAULT_ERROR_MSG;
        }
        req.setAttribute(OpenIdServletFilter.ERROR_MSG_ATTR, openIdErrorMsg);

        forwardLogin(req, resp);

    }

    protected void forwardHome(final HttpServletRequest req,
            final HttpServletResponse resp, OpenIdUser openIdUser)
            throws ServletException, IOException {
        preventCache(resp);
        setSessionOpenIdUser(req, openIdUser);
        req.getRequestDispatcher(homeUri).forward(req, resp);
    }

    protected void forwardLogin(final HttpServletRequest req,
            final HttpServletResponse resp) throws ServletException,
            IOException {
        setSessionOpenIdUser(req, null);
        setSessionUser(req, null);
        preventCache(resp);
        req.getRequestDispatcher(loginUri).forward(req, resp);
    }

    protected void generateConfigPage(final HttpServletRequest req,
            final HttpServletResponse resp) throws IOException {
        ServletOutputStream out = resp.getOutputStream();
        out.println("<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'>");
        out.println("<html>");
        out.println("  <head>");
        out.println("    <title>RoleCall Partner Configuration</title>");
        out.println("    <meta name='viewport' content='width=320, user-scalable=yes' />");
        out.println("  </head>");
        out.println("  <body>");
        out.println("    <h2>RoleCall Partner Configuration</h2>");

        String roleCallError = getSessionRoleCallError(req);
        if (null != roleCallError) {
            out.println("    <p style='color:red'>" + roleCallError + "</p>");
        }

        out.println("    <p>This site has not yet been configured to work with a RoleCall server.");
        out.println("        Please enter the unique title of this RoleCall partner, and the");
        out.println("        URL of the RoleCall server</p>");
        out.println("    <form method='POST'>");

        out.println("      <p>Unique title of this RoleCall partner on this server.</p>");
        String roleCallRelayTitle = getPropertyStore().get(
                "roleCallRelayTitle", "");
        out.println("      <input id='roleCallRelayTitle' value='"
                + roleCallRelayTitle.replace("'", "\"")
                + "' name='roleCallRelayTitle' type='text' size=80/>");

        out.println("      <p>URL of the RoleCall server.</p>");
        String roleCallUrl = getPropertyStore().get("roleCallUrl", "");
        out.println("      <input id='roleCallUrl' value='"
                + roleCallUrl.replace("'", "\"")
                + "' name='roleCallUrl' type='text' size=80/>");
        out.println("      <input type='submit' value='send'/>");
        out.println("    </form>");
        out.println("  </body>");
        out.println("<html>");
    }

    abstract protected PersistentPropertyStore getPropertyStore();

    protected String getSessionRoleCallError(HttpServletRequest req) {
        return (String) req.getSession().getAttribute("roleCallError");
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.servlet.GenericServlet#init(javax.servlet.ServletConfig)
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        this.homeUri = config.getInitParameter("homeUri");
        this.loginUri = config.getInitParameter("loginUri");
    }

    protected User openIdUserToUser(OpenIdUser openIdUser) {

        if (null == openIdUser) {
            return null;
        }
        Map<String, String> sreg10 = SRegExtension10.get(openIdUser);
        Map<String, String> sreg = SRegExtension.get(openIdUser);
        Map<String, String> axschema = AxSchemaExtension.get(openIdUser);
        Map<String, String> map = new HashMap<String, String>(
                (null == sreg10 ? 0 : sreg10.size())
                        + (null == sreg ? 0 : sreg.size())
                        + (null == axschema ? 0 : axschema.size()));
        if (null != sreg10)
            map.putAll(sreg10);
        if (null != sreg)
            map.putAll(sreg);
        if (null != axschema)
            map.putAll(axschema);
        String email = map.get("email");
        String fullname = map.get("fullname");
        ;
        if (null == fullname) {
            String firstname = map.get("firstname");
            String lastname = map.get("lastname");
            ;
            if (null != firstname) {
                fullname = firstname;
            }
            if (null != lastname) {
                if (null != firstname) {
                    fullname += " " + lastname;
                } else {
                    fullname = lastname;
                }
            }
        }
        User user = new User(email, fullname, openIdUser.getIdentifier());
        return user;
    }

    protected void preventCache(final HttpServletResponse resp) {
        // prevents caching at the proxy server
        resp.setHeader("Content-Type", "text/html");
        resp.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        resp.setHeader("Cache-Control", "post-check=0, pre-check=0");
        resp.setHeader("Pragma", "no-cache"); // HTTP 1.0
        resp.setDateHeader("Expires", 0);
        resp.setDateHeader("Last-Modified", (new Date()).getTime());
    }

    protected void reload(final HttpServletRequest req,
            final HttpServletResponse resp, OpenIdUser openIdUser)
            throws ServletException, IOException {
        setSessionOpenIdUser(req, openIdUser);
        StringBuffer requestURL = req.getRequestURL();
        addGwtCodeSvr(req, requestURL);
        resp.sendRedirect(requestURL.toString());
    }

    protected void setSessionOpenIdUser(final HttpServletRequest req,
            OpenIdUser openIdUser) {
        if (null != openIdUser) {
            req.getSession().setAttribute("openIdUser", openIdUser);
        } else {
            req.getSession().removeAttribute("openIdUser");
        }
    }

    protected void setSessionUser(final HttpServletRequest req, User user) {
        if (null != user) {
            req.getSession().setAttribute("user", user);
        } else {
            req.getSession().removeAttribute("user");
        }
    }

}
